package com.penglecode.netty.server;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import com.penglecode.netty.RequestEntity;
import com.penglecode.netty.ResponseEntity;

public class DefaultObjectMethodExecutor implements ObjectMethodExecutor<RequestEntity, ResponseEntity>, ApplicationContextAware {

	private static final Logger logger = LoggerFactory.getLogger(DefaultObjectMethodExecutor.class);
	
	/**
	 * key : interfaceName
	 * value : java.lang.reflect.Method
	 */
	private static final Map<String,ObjectMethodMetadata> methodMetadataCacheMap = new ConcurrentHashMap<String,ObjectMethodMetadata>();
	
	private ApplicationContext applicationContext;
	
	public ResponseEntity executeMethod(RequestEntity request) {
		ResponseEntity response = new ResponseEntity();
		try {
			if(request == null || !StringUtils.hasText(request.getInterfaceName()) || !StringUtils.hasText(request.getShortMethodName()) || !StringUtils.hasText(request.getFullMethodName())){
				response.setSuccess(false);
				response.setMsg("Some parameters must be required in RequestEntity!");
				return response;
			}else{
				ObjectMethodMetadata methodMetadata = methodMetadataCacheMap.get(request.getInterfaceName());
				Object targetConcreateBean = null;
				Method targetInvokeMethod = null;
				Class<?> interfaceClass = null;
				String interfaceMethodExpression = request.getShortMethodName() + request.getFullMethodName().substring(request.getFullMethodName().indexOf('('));
				if(methodMetadata == null){ // not found in cache
					interfaceClass = ClassUtils.forName(request.getInterfaceName(), Thread.currentThread().getContextClassLoader());
					try {
						targetConcreateBean  = applicationContext.getBean(interfaceClass);
					} catch (NoUniqueBeanDefinitionException e1) {
						response.setSuccess(false);
						response.setMsg(String.format("No unique bean which implemented %s found in spring applicationtext!", request.getInterfaceName()));
						return response;
					} catch (NoSuchBeanDefinitionException e2) {
						response.setSuccess(false);
						response.setMsg(String.format("No concreate bean which implemented %s found in spring applicationtext!", request.getInterfaceName()));
						return response;
					}
					final Class<?> concreateBeanClass = targetConcreateBean.getClass();
					targetInvokeMethod = findMethod(concreateBeanClass, request.getShortMethodName(), interfaceMethodExpression);
					if(targetInvokeMethod == null){
						response.setSuccess(false);
						response.setMsg(String.format("No suitable method[%s] found in concreate bean %s which implemented %s", request.getFullMethodName(), concreateBeanClass, request.getInterfaceName()));
						return response;
					}
					methodMetadata = new ObjectMethodMetadata();
					methodMetadata.setInterfaceClass(interfaceClass);
					methodMetadata.setMethodName(request.getShortMethodName());
					methodMetadata.addMethod(interfaceMethodExpression, targetInvokeMethod);
					methodMetadataCacheMap.put(request.getInterfaceName(), methodMetadata);
				}else{ // found in cache
					targetInvokeMethod = methodMetadata.getMethod(interfaceMethodExpression);
					interfaceClass = methodMetadata.getInterfaceClass();
					try {
						targetConcreateBean  = applicationContext.getBean(interfaceClass);
					} catch (NoUniqueBeanDefinitionException e1) {
						response.setSuccess(false);
						response.setMsg(String.format("No unique bean which implemented %s found in spring applicationtext!", request.getInterfaceName()));
						return response;
					} catch (NoSuchBeanDefinitionException e2) {
						response.setSuccess(false);
						response.setMsg(String.format("No concreate bean which implemented %s found in spring applicationtext!", request.getInterfaceName()));
						return response;
					}
					Class<?> concreateBeanClass = targetConcreateBean.getClass();
					if(targetInvokeMethod == null){
						targetInvokeMethod = findMethod(concreateBeanClass, request.getShortMethodName(), interfaceMethodExpression);
						if(targetInvokeMethod == null){
							response.setSuccess(false);
							response.setMsg(String.format("No suitable method[%s] found in concreate bean %s which implemented %s", request.getFullMethodName(), concreateBeanClass, request.getInterfaceName()));
							return response;
						}
						methodMetadata.addMethod(interfaceMethodExpression, targetInvokeMethod); // merge
					}else{
						logger.debug(">>> found cached method : " + targetInvokeMethod);
					}
				}
				Object returnValue = ReflectionUtils.invokeJdbcMethod(targetInvokeMethod, targetConcreateBean, request.getMethodArgs());
				response.setSuccess(true);
				response.setMsg("Invoke method successfully!");
				response.setReturnValue(returnValue);
				return response;
			}
		} catch (Throwable e) {
			logger.error(e.getMessage(), e);
			response.setSuccess(false);
			response.setMsg(e.getMessage());
			return response;
		}
	}
	
	/**
	 * find the target method in concreateBeanClass
	 * @param concreateBeanClass
	 * @param interfaceMethodName
	 * @param interfaceMethodExpression		- a example like this: getOrderInfo(java.lang.String)
	 * @return
	 */
	private Method findMethod(final Class<?> concreateBeanClass, final String interfaceMethodName, final String interfaceMethodExpression){
		Class<?> searchType = concreateBeanClass;
		while (searchType != null) {
			Method[] methods = (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods());
			for (Method method : methods) {
				String fullMethodName = method.toGenericString();
				fullMethodName = interfaceMethodName + fullMethodName.substring(fullMethodName.indexOf('('));
				if (fullMethodName.equals(interfaceMethodExpression)) {
					return method;
				}
			}
			searchType = searchType.getSuperclass();
		}
		return null;
	}

	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.applicationContext = applicationContext;
	}

}
